package icasue.runninginvoke.task.redis;

import icasue.base.utils.check.CommonCheck;
import icasue.base.utils.wareable.FieldConvertUtil;
import lombok.Data;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/11/11 下午3:40
 * @UpdateDate:
 * @Description:
 */
@Data
public class DateRedisTaskOperate extends AbstractRedisAsyncTask{

    private MethodHandle evalHandle;
    private MethodHandle scriptConstructHandle;
    private Object redisTemplate;
    private Object redisConnectionFactory;
    public static final MethodHandles.Lookup lookup = FieldConvertUtil.methodLookup;

    public DateRedisTaskOperate(Object redisConnectionFactory,String hashKeyHome){
        super();
        this.redisConnectionFactory = redisConnectionFactory;
        if(!CommonCheck.nullStrings(hashKeyHome)){
            super.hashKeyHome = hashKeyHome;
        }
        init();
    }

    public DateRedisTaskOperate(Object redisConnectionFactory){
        this(redisConnectionFactory,null);
    }

    @Override
    protected void init() {
        Class<?> redisTemplateMetadata = null;
        Class<?> redisConnectionFactoryMetadata = null;
        Class<?> redisAccessorMetadata = null;
        Class<?> redisOptionsMetadata = null;
        Class<?> redisScriptMetadata = null;
        Class<?> stringSerializerMetadata = null;
        Class<?> initializingBeanMetadata = null;
        try {
            // suggest data-redis 2.2.1
            redisTemplateMetadata = Class.forName("org.springframework.data.redis.core.RedisTemplate");
            redisConnectionFactoryMetadata = Class.forName("org.springframework.data.redis.connection.RedisConnectionFactory");
            redisAccessorMetadata = Class.forName("org.springframework.data.redis.core.RedisAccessor");
            redisOptionsMetadata = Class.forName("org.springframework.data.redis.core.RedisOperations");
            redisScriptMetadata = Class.forName("org.springframework.data.redis.core.script.RedisScript");
            stringSerializerMetadata = Class.forName("org.springframework.data.redis.serializer.StringRedisSerializer");
            initializingBeanMetadata = Class.forName("org.springframework.beans.factory.InitializingBean");
        }catch (Throwable e){
            throw new RuntimeException("DateRedisTaskOperate :: {} init occur an error, cause : class metadata not found, mayBe data-redis's version is duplicate, we suggest: 2.2.1",e.getCause());
        }
        try {
            if(!CommonCheck.nullObjects(redisTemplateMetadata,redisOptionsMetadata,
                    redisScriptMetadata,stringSerializerMetadata,redisConnectionFactory)) {
                if(!redisConnectionFactoryMetadata.isAssignableFrom(redisConnectionFactory.getClass())){
                    throw new RuntimeException("The instance of redisConnectionFactory which been given is not as a implement of "
                            + redisConnectionFactoryMetadata.toString());
                }
                //Clone redisTemplate, for confirm lua scriptExecutor exist and construct redisOption's valueSerializer to Long.
                Constructor<?> redisTemplateMetadataConstructor = redisTemplateMetadata.getConstructor();
                redisTemplateMetadataConstructor.setAccessible(true);
                Object genericTemplate = redisTemplateMetadataConstructor.newInstance();

                // Set redisConnectionFactory. which generic redisTemplate needed.
                Method setConnectionFactoryMethod = redisAccessorMetadata.getDeclaredMethod("setConnectionFactory",redisConnectionFactoryMetadata);
                setConnectionFactoryMethod.setAccessible(true);
                setConnectionFactoryMethod.invoke(genericTemplate,redisConnectionFactory);

                // Process post construct, from InitializingBean.
                Method afterPropertiesSetMethod = initializingBeanMetadata.getDeclaredMethod("afterPropertiesSet");
                afterPropertiesSetMethod.invoke(genericTemplate);

                // Set valueSerializer.
                Constructor<?> stringValueSerializerConstruct = stringSerializerMetadata.getConstructor();
                stringValueSerializerConstruct.setAccessible(true);
                Object stringValueSerializer = stringValueSerializerConstruct.newInstance();

                Field valueSerializerField = redisTemplateMetadata.getDeclaredField("valueSerializer");
                valueSerializerField.setAccessible(true);
                valueSerializerField.set(genericTemplate,stringValueSerializer);

                // Set template's state to init.
                Field initializedField = redisTemplateMetadata.getDeclaredField("initialized");
                initializedField.setAccessible(true);
                initializedField.set(genericTemplate,Boolean.TRUE);
                this.redisTemplate = genericTemplate;

                // Getting all invoke methods handle.
                try{
                    //Find redisTemplate's eval method handle.
                    evalHandle = lookup.findVirtual(redisOptionsMetadata,"execute", MethodType.methodType(Object.class, redisScriptMetadata, List.class, Object[].class));
                    //Find DefaultRedisScript's instance handle.
                    Constructor<?> defaultRedisScriptConstructor = Class.forName("org.springframework.data.redis.core.script.DefaultRedisScript")
                            .getDeclaredConstructor(String.class, Class.class);
                    defaultRedisScriptConstructor.setAccessible(true);
                    scriptConstructHandle = lookup.unreflectConstructor(defaultRedisScriptConstructor);
                }catch (Throwable e){
                    throw new RuntimeException("DateRedisTaskOperate {} init error when getting methodHandles process.");
                }
            }else {
                throw new RuntimeException("DateRedisTaskOperate {} init need a instance of RedisTemplate. ");
            }
        }catch (Throwable e){
            throw new RuntimeException("DateRedisTaskOperate :: {} init occur an error, cause: mayBe reflect is relationship to data-redis's version. ",e.getCause());
        }
    }

    @Override
    public Boolean touchExecute(String taskKey) {
        try {
            Object touchExecuteLuaScript = scriptConstructHandle.invoke(TOUCH_EXECUTE_LUA, Long.class);
            Object touchResult = evalHandle.bindTo(redisTemplate).invoke(touchExecuteLuaScript, Arrays.asList(this.hashKeyHome,taskKey), new Object[]{"1"});
            if(touchResult.equals(Long.valueOf(1))){
                return Boolean.TRUE;
            }
        }catch (Throwable e){
            throw new RuntimeException("DateRedisTaskOperate :: touchExecute :: execute occur an error.",e.getCause());
        }
        return Boolean.FALSE;
    }

    @Override
    public String getExecute(String taskKey) {
        try {
            Object getExecuteLuaScript = scriptConstructHandle.invoke(GET_EXECUTE_LUA, String.class);
            Object getResult = evalHandle.bindTo(redisTemplate).invoke(getExecuteLuaScript, Arrays.asList(this.hashKeyHome,taskKey), new Object[]{});
            return (String)getResult;
        }catch (Throwable e){
            throw new RuntimeException("DateRedisTaskOperate :: getExecute :: execute occur an error.",e.getCause());
        }
    }

    @Override
    public Boolean destroyExecute(String taskKey) {
        try {
            Object destroyExecuteLuaScript = scriptConstructHandle.invoke(DESTROY_EXECUTE_LUA, Long.class);
            Object destroyTaskResult = evalHandle.bindTo(redisTemplate).invoke(destroyExecuteLuaScript, Arrays.asList(this.hashKeyHome,taskKey), new Object[]{});
            return Boolean.TRUE;
        }catch (Throwable e){
            throw new RuntimeException("DateRedisTaskOperate :: touchExecute :: execute occur an error.",e.getCause());
        }
    }

    @Override
    public Boolean updateIdentity(String taskKey, String identity) {
        try {
            Object updateIdentityLuaScript = scriptConstructHandle.invoke(UPDATE_TASK_IDENTITY, Long.class);
            Object updateIdentityResult = evalHandle.bindTo(redisTemplate).invoke(updateIdentityLuaScript, Arrays.asList(this.hashKeyHome,taskKey), new Object[]{identity});
            return Boolean.TRUE;
        }catch (Throwable e){
            throw new RuntimeException("DateRedisTaskOperate :: touchExecute :: execute occur an error.",e.getCause());
        }
    }

    @Override
    public void clear() {
        try {
            Object clearPreviousIdentityLuaScript = scriptConstructHandle.invoke(CLEAR_TASK_IDENTITY, Long.class);
            Object clearPreviousIdentityResult = evalHandle.bindTo(redisTemplate).invoke(clearPreviousIdentityLuaScript, Arrays.asList(this.hashKeyHome), new Object[]{});
        }catch (Throwable e){
            throw new RuntimeException("DateRedisTaskOperate :: clear :: execute occur an error.",e.getCause());
        }
    }
}
